#include "downloader.h"
#include "base/log/logger.h"
#include "base/utils/utils.h"
#include "service.h"
#include <QFile>
#include <QFileInfo>
#include <QNetworkAccessManager>
#include <QtDebug>
#include <QTimer>
#include <QUrl>

namespace yc {

Downloader::Downloader(Service* service, const QString& service_url, const QDir& local_dir,
                       bool auto_delete, int timeout, QObject* parent)
    : Downloader(service, service_url, QList<DownloadResource> {}, local_dir, 0, auto_delete,
                 timeout, 1, parent)
{
}

Downloader::Downloader(Service* service, const QString& service_url,
                       const QList<DownloadResource>& resources, const QDir& local_dir,
                       QObject* parent)
    : Downloader(service, service_url, resources, local_dir, 0, true, 0, 1, parent)
{
}

Downloader::Downloader(Service* service, const QString& service_url,
                       const QList<DownloadResource>& resources, const QDir& local_dir, int id,
                       bool auto_delete, int timeout, int try_times, QObject* parent)
    : Task(id, auto_delete, timeout, parent),
      service_(service),
      ignore_failure_(false),
      service_url_(service_url),
      resources_(resources),
      local_dir_(local_dir),
      file_(nullptr),
      bytes_received_(0),
      bytes_total_(0),
      has_timeout_option_(timeout > 0),
      try_times_(try_times),
      single_timer_(nullptr),
      single_timer_started_(false)
{
    for (const DownloadResource& r : resources) {
        bytes_total_ += r.bytes_total;
    }
    has_bytes_total_ = (bytes_total_ > 0);
}

Downloader::~Downloader()
{
    Downloader::terminateImpl();
}

const QList<DownloadResource>& Downloader::resources() const
{
    return resources_;
}

void Downloader::setHeaders(const QMap<QString, QString>& headers)
{
    headers_ = headers;
}

void Downloader::ignoreFailure(bool setting)
{
    ignore_failure_ = setting;
}

void Downloader::addResource(const DownloadResource& resource)
{
    bool found = false;
    for (const DownloadResource& r : std::as_const(resources_)) {
        if (r.local_name == resource.local_name) {
            found = true;
            break;
        }
    }
    if (!found) {
        resources_.append(resource);
        if (!isRunning()) {
            start();
        }
    }
}

QString Downloader::statusMessage() const
{
    return tr("Downloading...");
}

qint64 Downloader::evaluateTotalMSecs() const
{
    return (bytes_total_ > 0 ? Service::downloadCostMSecs(bytes_total_) : 1000);
}

void Downloader::startImpl()
{
    startNextRequest();
}

void Downloader::terminateImpl()
{
    if (reply_) {
        disconnect(reply_, nullptr, this, nullptr);
        if (reply_->isRunning()) {
            reply_->abort();
        }
        delete reply_;
        reply_ = nullptr;
    }
    closeFile();
    stopSingleTimer();
}

void Downloader::onTimeout()
{
    if (reply_) {
        disconnect(reply_, nullptr, this, nullptr);
        if (reply_->isRunning()) {
            reply_->abort();
        }
        reply_->deleteLater();
        reply_ = nullptr;
    }
    closeFile();
    stopSingleTimer();
}

void Downloader::closeFile(bool remove_file)
{
    if (file_) {
        file_->flush();
        file_->close();
        if (remove_file) {
            file_->remove();
        }
        delete file_;
        file_ = nullptr;
    }
}

void Downloader::startNextRequest()
{
    if (resources_.empty()) {
        Q_EMIT finished();
        return;
    }

    const DownloadResource& res = resources_.takeFirst();
    if (res.local_name.isEmpty()) {
        startNextRequest();
        return;
    }
    QString file_name;
    if (res.local_name.startsWith('/') || res.local_name.contains(':')
        || res.local_name.startsWith('\\')) {
        file_name = res.local_name;
    } else {
        file_name = local_dir_.absoluteFilePath(res.local_name);
    }
    QString std_path = file_name.replace("//", "/").replace("\\\\", "\\");
    QFileInfo fi(std_path);
    QDir file_dir(fi.absoluteDir());
    if (!file_dir.exists() && !QDir().mkpath(file_dir.absolutePath())) {
        logCritical() << "can't mk dir " << file_dir;
        Q_EMIT failed(LOGIC_ERROR, "Can't mk dir.");
        return;
    }

    QString tmp_file_name = file_name + ".tmp";
    QFile* file = new QFile(tmp_file_name);
    if (!file->open(QFile::WriteOnly | QFile::Truncate)) {
        logCritical() << "can't open local file " << tmp_file_name;
        delete file;
        Q_EMIT failed(LOGIC_ERROR, "Can't open local file.");
        return;
    }
    file_ = file;
    added_total_ = false;
    current_bytes_total_ = 0;
    current_try_times_ = try_times_;

    QString full_url = baseutils::fullUrl(service_url_, res.url);
    QNetworkRequest request;
    Service::setupRequest(request, full_url, "application/octet-stream");
    for (auto it = service_->headers().constBegin(), end = service_->headers().constEnd();
         it != end; it++) {
        request.setRawHeader(it.key().toUtf8(), it.value().toUtf8());
    }
    for (auto it = headers_.constBegin(), end = headers_.constEnd(); it != end; it++) {
        request.setRawHeader(it.key().toUtf8(), it.value().toUtf8());
    }

    startSingleTimer(360000);
    current_ = res;
    reply_ = service_->networkAccessManager()->get(request);
    connect(reply_, &QNetworkReply::readyRead, this, [this] {
        if (file_) {
            file_->write(reply_->readAll());
        }
    });
    connect(reply_, &QNetworkReply::finished, this, [this] {
        bytes_received_ += current_bytes_total_;
        stopSingleTimer();
        QString tmp_file_name;
        if (file_) {
            QByteArray ba = reply_->readAll();
            file_->write(ba);
            tmp_file_name = file_->fileName();
        }
        closeFile();
        if (!tmp_file_name.isEmpty()) {
            const QString file_name = tmp_file_name.left(tmp_file_name.length() - 4);
            QFile::remove(file_name);
            QFile::rename(tmp_file_name, file_name);
            Q_EMIT downloaded(file_name);
        }
        disconnect(reply_, nullptr, this, nullptr);
        reply_->deleteLater();
        reply_ = nullptr;
        startNextRequest();
    });
    connect(reply_, &QNetworkReply::errorOccurred, this, [this](QNetworkReply::NetworkError error) {
        logCritical() << "download" << reply_->url() << error << ". msg:" << reply_->readAll();
        disconnect(reply_, nullptr, this, nullptr);
        stopSingleTimer();
        const QString message = reply_->errorString();
        reply_->deleteLater();
        reply_ = nullptr;
        closeFile(true);
        if (ignore_failure_) {
            startNextRequest();
        } else {
            if (--current_try_times_ > 0) {
                qDebug() << "try to download resource" << current_.url << current_try_times_;
                int cur_try_times = current_try_times_;
                if (added_total_) {
                    bytes_total_ -= current_bytes_total_;
                }
                resources_.push_front(current_);
                startNextRequest();
                current_try_times_ = cur_try_times;
            } else {
                Q_EMIT failed(error, message);
            }
        }
    });
    connect(reply_, &QNetworkReply::downloadProgress, this,
            [this](qint64 bytes_received, qint64 bytes_total) {
                current_bytes_total_ = bytes_total;
                if (!has_bytes_total_ && !added_total_) {
                    bytes_total_ += bytes_total;
                    added_total_ = true;
                }
                qint64 bytes_left = bytes_total - bytes_received;
                qint64 max_eval_timeout = 360000 + Service::downloadCostMSecs(bytes_left);
                startSingleTimer(static_cast<int>(max_eval_timeout));
                qint64 msecs = elapsedMSecs();
                if (msecs > 50) {
                    qint64 bytes_total_received = bytes_received_ + bytes_received;
                    qreal seconds = msecs * 0.001;
                    Service::setDownloadSpeed(bytes_total_received / seconds);
                }
                Q_EMIT progress(bytes_received_ + bytes_received, bytes_total_);
            });
}

void Downloader::startSingleTimer(int interval, int deviation)
{
    if (!single_timer_) {
        single_timer_ = new QTimer(this);
        connect(single_timer_, &QTimer::timeout, this, [this] {
            stopSingleTimer();
            if (reply_) {
                disconnect(reply_, nullptr, this, nullptr);
                if (reply_->isRunning()) {
                    reply_->abort();
                }
                reply_->deleteLater();
                reply_ = nullptr;
            }
            closeFile(true);
            if (ignore_failure_) {
                startNextRequest();
            } else {
                if (--current_try_times_ > 0) {
                    qDebug() << "try to download resource" << current_.url << current_try_times_;
                    int cur_try_times = current_try_times_;
                    if (added_total_) {
                        bytes_total_ -= current_bytes_total_;
                    }
                    resources_.push_front(current_);
                    startNextRequest();
                    current_try_times_ = cur_try_times;
                } else {
                    qDebug() << "download resource timeout" << current_.url;
                    Q_EMIT failed(TIMEOUT_ERROR, tr("Timeout"));
                }
            }
        });
    }
    if (single_timer_started_) {
        int prev = single_timer_->interval();
        int d = prev - interval;
        if (qAbs(d) > deviation) {
            single_timer_started_ = true;
            single_timer_->start(interval);
        }
    } else {
        single_timer_started_ = true;
        single_timer_->start(interval);
    }
}

void Downloader::stopSingleTimer()
{
    if (single_timer_) {
        single_timer_->stop();
        single_timer_started_ = false;
    }
}

} // namespace yc
